/*
 * @(#)MacEditorKit.java  1.0  December 1, 2004
 *
 * Copyright (c) 2004 Werner Randelshofer
 * Staldenmattweg 2, Immensee, CH-6405, Switzerland.
 * All rights reserved.
 *
 * The copyright of this software is owned by Werner Randelshofer. 
 * You may not use, copy or modify this software, except in  
 * accordance with the license agreement you entered into with  
 * Werner Randelshofer. For details see accompanying license terms. 
 *
 * Part of this software (as marked) has been derived from software by
 * Dustin Sacks. These parts are used under license.
 */
package com.seaglass.util;

import java.awt.Component;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.util.HashMap;

import javax.swing.Action;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.JTextComponent;
import javax.swing.text.TextAction;
import javax.swing.text.Utilities;

/**
 * The MacEditorKit extends the Swing DefaultEditorKit with Mac OS X specific
 * text editing actions.
 * 
 * @author Werner Randelshofer
 * @version 1.0 December 1, 2004 Created.
 */
public class MacEditorKit extends DefaultEditorKit {

    /**
     * Name of the action to delete the word that precedes the current caret
     * position.
     * 
     * @see #getActions
     */
    public static final String    deletePrevWordAction = "delete-previous-word";

    /**
     * Name of the action to delete the word that follows the current caret
     * position.
     * 
     * @see #getActions
     */
    public static final String    deleteNextWordAction = "delete-next-word";

    /**
     * Default actions of the MacEditorKit.
     */
    private static final Action[] actions;
    static {
        Action[] dekActions = new DefaultEditorKit().getActions();
        HashMap dekActionMap = new HashMap();
        for (int i = 0; i < dekActions.length; i++) {
            dekActionMap.put(dekActions[i].getValue(Action.NAME), dekActions[i]);
        }

        HashMap actionMap = (HashMap) dekActionMap.clone();
        actionMap.put(deleteNextWordAction, new MacEditorKit.DeleteNextWordAction());
        actionMap.put(deletePrevWordAction, new MacEditorKit.DeletePrevWordAction());
        actionMap.put(upAction, new MacEditorKit.VerticalAction(upAction, (TextAction) dekActionMap.get(upAction),
            (TextAction) dekActionMap.get(beginAction)));
        actionMap.put(downAction, new MacEditorKit.VerticalAction(downAction, (TextAction) dekActionMap.get(downAction),
            (TextAction) dekActionMap.get(endAction)));
        actionMap.put(selectionUpAction, new MacEditorKit.VerticalAction(selectionUpAction, (TextAction) dekActionMap
            .get(selectionUpAction), (TextAction) dekActionMap.get(selectionBeginAction)));
        actionMap.put(selectionDownAction, new MacEditorKit.VerticalAction(selectionDownAction, (TextAction) dekActionMap
            .get(selectionDownAction), (TextAction) dekActionMap.get(selectionEndAction)));

        actions = (Action[]) actionMap.values().toArray(new Action[0]);

        // TO DO: Use this instead of the code above:
        // actions = TextAction.augmentList(....)
    }

    /**
     * Default constructor.
     */
    public MacEditorKit() {
    }

    /**
     * Fetches the set of commands that can be used on a text component that is
     * using a model and view produced by this kit.
     * 
     * @return the command list
     */
    public Action[] getActions() {
        return actions;
    }

    /*
     * Deletes the word that follows the current caret position.
     * 
     * Original code of this class by Dustin Sacks.
     * 
     * @see MacEditorKit#deleteNextWordAction
     * 
     * @see MacEditorKit#getActions
     */
    static class DeleteNextWordAction extends TextAction {
        /**
         * Creates this object with the appropriate identifier.
         */
        DeleteNextWordAction() {
            super(deleteNextWordAction);
        }

        public void actionPerformed(ActionEvent e) {
            JTextComponent target = getTextComponent(e);
            boolean beep = true;
            if ((target != null) && (target.isEditable())) {
                try {
                    // select the next word
                    int offs = target.getCaretPosition();
                    int endOffs;
                    String s = target.getDocument().getText(offs, 1);
                    if (Character.isWhitespace(s.charAt(0))) {
                        endOffs = Utilities.getNextWord(target, offs);
                        endOffs = Utilities.getWordEnd(target, endOffs);
                    } else {
                        endOffs = Utilities.getWordEnd(target, offs);
                    }
                    target.moveCaretPosition(endOffs);

                    // and then delete it
                    target.replaceSelection("");
                    beep = false;
                } catch (BadLocationException exc) {
                    // nothing to do, because we set beep to true already
                }
            }
            if (beep) {
                provideErrorFeedback(target);
            }
        }
    }

    /*
     * Deletes the word that precedes the current caret position.
     * 
     * Original code of this class by Dustin Sacks.
     * 
     * @see MacEditorKit#deletePrevWordAction
     * 
     * @see MacEditorKit#getActions
     */
    static class DeletePrevWordAction extends TextAction {
        /**
         * Creates this object with the appropriate identifier.
         */
        DeletePrevWordAction() {
            super(deletePrevWordAction);
        }

        public void actionPerformed(ActionEvent e) {
            JTextComponent target = getTextComponent(e);
            boolean beep = true;
            if ((target != null) && (target.isEditable())) {
                int offs = target.getCaretPosition();
                boolean failed = false;
                try {
                    offs = Utilities.getPreviousWord(target, offs);
                } catch (BadLocationException bl) {
                    if (offs != 0) {
                        offs = 0;
                    } else {
                        failed = true;
                    }
                }
                if (!failed) {
                    target.moveCaretPosition(offs);
                    // and then delete it
                    target.replaceSelection("");
                    beep = false;
                }
            }
            if (beep) {
                provideErrorFeedback(target);
            }
        }
    }

    /*
     * Action to move the selection up or down.
     * 
     * This is very similar to the NextVisualPositionAction of class
     * DefaultEditorKit. The differences is, that we move the cursor to the
     * beginning of the text, if the user wants to move upwards and is already
     * at the first line of the text. We move the cursor to the end of the text,
     * if the user wants to move downwards and is already at the last line of
     * the text.
     * 
     * 
     * Note that we delegate actions to DefaultEditorKit actions. We can not
     * implement all the required code by ourself, because method
     * DefaultCaret.getDotBias() is not accessible from outside the
     * javax.swing.text package.
     */
    static class VerticalAction extends TextAction {
        private TextAction verticalAction;
        private TextAction beginEndAction;

        /**
         * Create this action with the appropriate identifier.
         */
        VerticalAction(String name, TextAction verticalAction, TextAction beginEndAction) {
            super(name);
            this.verticalAction = verticalAction;
            this.beginEndAction = beginEndAction;
        }

        /** The operation to perform when this action is triggered. */
        public void actionPerformed(ActionEvent e) {
            JTextComponent target = getTextComponent(e);
            if (target != null) {
                // target.getUI().getNextVisualPositionFrom(t
                Caret caret = target.getCaret();
                int dot = caret.getDot();
                verticalAction.actionPerformed(e);
                if (dot == caret.getDot()) {
                    Point magic = caret.getMagicCaretPosition();
                    beginEndAction.actionPerformed(e);
                    caret.setMagicCaretPosition(magic);
                }
            }
        }
    }

    /**
     * Invoked when the user attempts an invalid operation, such as pasting into
     * an uneditable <code>JTextField</code> that has focus. The default
     * implementation beeps. Subclasses that wish different behavior should
     * override this and provide the additional feedback.
     * 
     * @param component
     *            Component the error occured in, may be null indicating the
     *            error condition is not directly associated with a
     *            <code>Component</code>.
     */
    static void provideErrorFeedback(Component component) {
        Toolkit toolkit = null;
        if (component != null) {
            toolkit = component.getToolkit();
        } else {
            toolkit = Toolkit.getDefaultToolkit();
        }
        toolkit.beep();
    } // provideErrorFeedback()
}
